import { ethers } from "ethers";
import { groupBy, mapValues, uniqBy } from "lodash";
const ICONS_CDN = "https://icons.llamao.fi/icons";

export const chainsMap = {
  ethereum: 1,
  bsc: 56,
  polygon: 137,
  optimism: 10,
  arbitrum: 42161,
  avax: 43114,
  gnosis: 100,
  fantom: 250,
  klaytn: 8217,
  aurora: 1313161554,
  celo: 42220,
  cronos: 25,
  dogechain: 2000,
  moonriver: 1285,
  bttc: 199,
  oasis: 42262,
  velas: 106,
  heco: 128,
  harmony: 1666600000,
  boba: 288,
  okexchain: 66,
  fuse: 122,
  moonbeam: 1284,
  canto: 7700,
};

export const geckoChainsMap = {
  ethereum: 1,
  "binance-smart-chain": 56,
  "polygon-pos": 137,
  "optimistic-ethereum": 10,
  "arbitrum-one": 42161,
  avalanche: 43114,
  xdai: 100,
  fantom: 250,
  "klay-token": 8217,
  aurora: 1313161554,
  celo: 42220,
  cronos: 25,
  dogechain: 2000,
  moonriver: 1285,
  bittorrent: 199,
  oasis: 42262,
  velas: 106,
  heco: 128,
  "harmony-shard-0": 1666600000,
  boba: 288,
  "okex-chain": 66,
  fuse: 122,
  moonbeam: 1284,
  canto: 7700,
};

export function chainIconUrl(chain) {
  return `${ICONS_CDN}/agg_icons/${chain.toLowerCase()}?w=24&h=24`;
}

export function protoclIconUrl(protocol) {
  return `${ICONS_CDN}/protocols/${protocol}?w=24&h=24`;
}

const ethereum = {
  mcap: Number.MAX_SAFE_INTEGER,
  address: ethers.constants.AddressZero,
  chainId: 1,
  name: "Ethereum",
  symbol: "ETH",
  icon: chainIconUrl("ethereum"),
  decimals: 18,
};

const binance = {
  mcap: Number.MAX_SAFE_INTEGER,
  address: ethers.constants.AddressZero,
  chainId: 56,
  name: "Binance",
  symbol: "BNB",
  icon: chainIconUrl("binance"),
  decimals: 18,
};

const arbitrum = {
  mcap: Number.MAX_SAFE_INTEGER,
  address: ethers.constants.AddressZero,
  chainId: 42161,
  name: "Ethereum",
  symbol: "ETH",
  icon: chainIconUrl("ethereum"),
  decimals: 18,
};

const optimism = {
  mcap: Number.MAX_SAFE_INTEGER,
  address: ethers.constants.AddressZero,
  chainId: 10,
  name: "Ethereum",
  symbol: "ETH",
  icon: chainIconUrl("ethereum"),
  decimals: 18,
};

const okx = {
  mcap: Number.MAX_SAFE_INTEGER,
  address: ethers.constants.AddressZero,
  chainId: 66,
  name: "OKX",
  symbol: "OKX",
  icon: chainIconUrl("okexchain"),
  decimals: 18,
};

const boba = {
  mcap: Number.MAX_SAFE_INTEGER,
  address: ethers.constants.AddressZero,
  chainId: 288,
  icon: chainIconUrl("ethereum"),
  name: "Ethereum",
  symbol: "ETH",
  decimals: 18,
};

const harmony = {
  mcap: Number.MAX_SAFE_INTEGER,
  address: ethers.constants.AddressZero,
  chainId: 1666600000,
  icon: chainIconUrl("harmony"),
  decimals: 18,
  name: "Harmony",
  symbol: "ONE",
};

const heco = {
  mcap: Number.MAX_SAFE_INTEGER,
  address: ethers.constants.AddressZero,
  chainId: 128,
  icon: chainIconUrl("heco"),
  name: "Huobi Token",
  symbol: "HT",
};

const velas = {
  mcap: Number.MAX_SAFE_INTEGER,
  address: ethers.constants.AddressZero,
  chainId: 106,
  icon: chainIconUrl("velas"),
  name: "Velas",
  symbol: "VLX",
};

const oasis = {
  mcap: Number.MAX_SAFE_INTEGER,
  address: ethers.constants.AddressZero,
  chainId: 24462,
  name: "Oasis",
  symbol: "ROSE",
  icon: chainIconUrl("oasis"),
  decimals: 18,
};

const bttc = {
  mcap: Number.MAX_SAFE_INTEGER,
  address: ethers.constants.AddressZero,
  chainId: 199,
  name: "BitTorrent",
  icon: chainIconUrl("bittorrent"),
  symbol: "BTT",
  decimals: 18,
};

const moonriver = {
  mcap: Number.MAX_SAFE_INTEGER,
  address: ethers.constants.AddressZero,
  chainId: 1285,
  name: "MoonRiver",
  icon: chainIconUrl("moonriver"),
  symbol: "MOVR",
  decimals: 18,
};

const moonbeam = {
  mcap: Number.MAX_SAFE_INTEGER,
  address: ethers.constants.AddressZero,
  chainId: 1284,
  name: "Moonbeam",
  icon: chainIconUrl("moonbeam"),
  symbol: "GLMR",
  decimals: 18,
};

const fuse = {
  mcap: Number.MAX_SAFE_INTEGER,
  address: ethers.constants.AddressZero,
  chainId: 122,
  name: "Fuse",
  icon: chainIconUrl("fuse"),
  symbol: "FUSE",
  decimals: 18,
};

const dogechain = {
  mcap: Number.MAX_SAFE_INTEGER,
  address: ethers.constants.AddressZero,
  chainId: 2000,
  name: "Doge",
  symbol: "DOGE",
  decimals: 18,
  icon: chainIconUrl("dogechain"),
};

const cronos = {
  mcap: Number.MAX_SAFE_INTEGER,
  address: ethers.constants.AddressZero,
  chainId: 25,
  name: "Cronos",
  symbol: "CRO",
  icon: chainIconUrl("cronos"),
  decimals: 18,
};
const celo = {
  mcap: Number.MAX_SAFE_INTEGER,
  address: ethers.constants.AddressZero,
  chainId: 42220,
  name: "Celo",
  symbol: "CELO",
  icon: chainIconUrl("celo"),
  decimals: 18,
};
const aurora = {
  mcap: Number.MAX_SAFE_INTEGER,
  address: ethers.constants.AddressZero,
  chainId: 1313161554,
  name: "Ethereum",
  symbol: "ETH",
  icon: chainIconUrl("ethereum"),
  decimals: 18,
};
const avax = {
  mcap: Number.MAX_SAFE_INTEGER,
  address: ethers.constants.AddressZero,
  chainId: 43114,
  icon: chainIconUrl("avax"),
  name: "Avalanche",
  symbol: "AVAX",
  decimals: 18,
};

const klaytn = {
  mcap: Number.MAX_SAFE_INTEGER,
  address: ethers.constants.AddressZero,
  chainId: 8217,
  name: "Klaytn",
  symbol: "KLAY",
  icon: chainIconUrl("klaytn"),
  decimals: 18,
};
const fantom = {
  mcap: Number.MAX_SAFE_INTEGER,
  address: ethers.constants.AddressZero,
  chainId: 250,
  icon: chainIconUrl("fantom"),
  name: "Fantom",
  symbol: "FTM",
  decimals: 18,
};

const gnosis = {
  mcap: Number.MAX_SAFE_INTEGER,
  address: ethers.constants.AddressZero,
  chainId: 100,
  name: "xDai",
  symbol: "xDai",
  icon: chainIconUrl("gnosis"),
  decimals: 18,
};
const polygon = {
  mcap: Number.MAX_SAFE_INTEGER,
  address: ethers.constants.AddressZero,
  chainId: 137,
  name: "Matic",
  symbol: "MATIC",
  icon: chainIconUrl("polygon"),
  decimals: 18,
};

const canto = {
  mcap: Number.MAX_SAFE_INTEGER,
  address: ethers.constants.AddressZero,
  chainId: 7700,
  name: "Canto",
  symbol: "CANTO",
  icon: chainIconUrl("canto"),
  decimals: 18,
};

export const nativeTokens = [
  ethereum,
  arbitrum,
  binance,
  optimism,
  polygon,
  oasis,
  fantom,
  velas,
  harmony,
  gnosis,
  klaytn,
  avax,
  aurora,
  cronos,
  celo,
  dogechain,
  moonriver,
  bttc,
  heco,
  boba,
  okx,
  moonbeam,
  fuse,
  canto,
];

const tokensToRemove = {
  1: {
    ["0xB8c77482e45F1F44dE1745F52C74426C631bDD52".toLowerCase()]: true,
  },
};

const oneInchChains = {
  ethereum: 1,
  bsc: 56,
  polygon: 137,
  optimism: 10,
  arbitrum: 42161,
  avax: 43114,
  gnosis: 100,
  fantom: 250,
  klaytn: 8217,
};

export const fixTotkens = (tokenlist) => {
  // OKX token logo
  tokenlist[66][0].icon = tokenlist[66][1].icon;
  // BTC -> BTC.b
  tokenlist[43114].find(
    ({ address }) => address.toLowerCase() === "0x152b9d0fdc40c096757f570a51e494bd4b943e50"
  ).symbol = "BTC.b";
  //RSR address
  // tokenlist[1].find(({ address }) => address.toLowerCase() === '0x8762db106b2c2a0bccb3a80d1ed41273552616e8').address =
  // 	'0x320623b8e4ff03373931769a31fc52a4e78b5d70';
  // XDAI -> DAI
  tokenlist[1].find(
    ({ address }) => address.toLowerCase() === "0x6b175474e89094c44da98b954eedeac495271d0f"
  ).symbol = "DAI";

  tokenlist[1].find(
    ({ address }) =>
      address.toLowerCase() === "0x249cA82617eC3DfB2589c4c17ab7EC9765350a18".toLowerCase()
  ).icon = protoclIconUrl("verse");

  return tokenlist;
};

export const getTopTokensByChain = async (chainId) => {
  try {
    if (!dexToolsChainMap[chainId]) {
      throw new Error(`${chainId} not supported by dex tools.`);
    }

    const res = await fetch(
      `https://www.dextools.io/shared/analytics/pairs?limit=200&interval=24h&chain=${dexToolsChainMap[chainId]}`
    ).then((res) => res.json());

    return [chainId, res.data || []];
  } catch (error) {
    return [chainId, []];
  }
};

export async function getTokenList() {
  const oneInch = await Promise.all(
    Object.values(oneInchChains).map(async (chainId) =>
      fetch(`https://tokens.1inch.io/v1.1/${chainId}`).then((r) => r.json())
    )
  );
  const [sushiList, geckoList, logos, ownList] = await Promise.all([
    fetch("https://token-list.sushi.com/").then((r) => r.json()),
    fetch("https://defillama-datasets.llama.fi/tokenlist/all.json").then((res) => res.json()),
    fetch("https://defillama-datasets.llama.fi/tokenlist/logos.json").then((res) => res.json()),
    fetch("https://raw.githubusercontent.com/0xngmi/tokenlists/master/canto.json").then((res) =>
      res.json()
    ),
  ]);

  const oneInchList = Object.values(oneInchChains)
    .map((chainId, i) =>
      Object.values(oneInch[i]).map((token) => ({
        ...token,
        chainId,
      }))
    )
    .flat();

  const tokensByChain = mapValues(
    groupBy([...nativeTokens, ...sushiList.tokens, ...oneInchList, ...ownList], "chainId"),
    (val) => uniqBy(val, (token) => token.address.toLowerCase())
  );

  let tokensFiltered = mapValues(tokensByChain, (val, key) => {
    return val
      .filter(
        (token) =>
          typeof token.address === "string" && !tokensToRemove[key]?.[token.address.toLowerCase()]
      )
      .map((token) => ({ ...token, address: token.address.toLowerCase() }));
  });

  tokensFiltered = fixTotkens(tokensFiltered);

  // get top tokens on each chain
  const topTokensByChain = await Promise.allSettled(
    Object.keys(tokensFiltered).map((chain) => getTopTokensByChain(chain))
  );

  const topTokensByVolume = Object.fromEntries(
    topTokensByChain
      .map((chain) => (chain.status === "fulfilled" ? chain.value : null))
      .filter((chain) => chain !== null && chain[1].length > 0 && tokensFiltered[chain[0]])
  );

  // store unique tokens by chain
  const uniqueTokenList = {};

  for (const chain in tokensFiltered) {
    tokensFiltered[chain].forEach((token) => {
      if (!uniqueTokenList[chain]) {
        uniqueTokenList[chain] = new Set();
      }

      uniqueTokenList[chain].add(token.address);
    });
  }

  // store coingecko tokens that aren't in above token list by chain
  const geckoListByChain = {};

  if (geckoList && geckoList.length > 0) {
    geckoList.forEach((geckoToken) => {
      Object.entries(geckoToken.platforms || {}).forEach(([chain, address]) => {
        const id = geckoChainsMap[chain];

        if (id && !uniqueTokenList[String(id)]?.has(address.toLowerCase())) {
          if (!geckoListByChain[id]) {
            geckoListByChain[id] = new Set();
          }

          geckoListByChain[id].add(address.toLowerCase());
        }
      });
    });
  }

  // fetch name, symbol, decimals fo coingecko tokens
  const geckoTokensList = await Promise.allSettled(
    Object.entries(geckoListByChain)
      .filter(([chain]) => chain !== "43114")
      .map(([chain, tokens]) => getTokensData([chain, Array.from(tokens || new Set())]))
  );

  const formatAndSortTokens = (tokens, chain) => {
    return tokens
      .map((t) => {
        const geckoId =
          geckoList && geckoList.length > 0
            ? geckoList.find((geckoCoin) => geckoCoin.symbol === t.symbol?.toLowerCase())?.id ??
              null
            : null;

        const volume24h =
          topTokensByVolume[chain]?.find((item) => item["_id"]?.token === t.address)?.[
            "volume24h"
          ] ?? 0;

        return {
          ...t,
          label: t.symbol,
          value: t.address,
          geckoId,
          icon:
            t.ownicon ||
            `https://icons.llamao.fi/icons/tokens/${t.chainId}/${t.address}?h=20&w=20`,
          icon2: t.icon || logos[geckoId] || null,
          volume24h,
        };
      })
      .sort((a, b) => (b.address === ethers.constants.AddressZero ? 1 : b.volume24h - a.volume24h));
  };

  // store coingecko token lists by chain
  const cgList = {};
  geckoTokensList.forEach((data) => {
    if (data.status === "rejected") return;

    const [chain, tokens] = data.value;

    if (!cgList[chain]) {
      cgList[chain] = [];
    }

    cgList[chain] = formatAndSortTokens(tokens || [], chain);
  });

  // format and store final tokens list
  let tokenlist = {};
  for (const chain in tokensFiltered) {
    tokenlist[chain] = [
      ...formatAndSortTokens(tokensFiltered[chain] || [], chain),
      ...(cgList[chain] || []),
    ];
  }
  console.log("tokenList", JSON.stringify(tokenlist));
  return tokenlist;
}

export const chainIdToName = (chainId) => {
  return Object.entries(chainsMap).find(([, id]) => String(id) === String(chainId))?.[0];
};

// use multicall to fetch tokens name, symbol and decimals
const getTokensData = async ([chainId, tokens]) => {
  const chainName = chainIdToName(chainId);

  if (process.env.NODE_ENV === "development") {
    return [chainId, []];
  }

  if (!chainName) {
    return [chainId, []];
  }

  const { output: names } = await multiCall({
    abi: {
      constant: true,
      inputs: [],
      name: "name",
      outputs: [
        {
          name: "",
          type: "string",
        },
      ],
      payable: false,
      stateMutability: "view",
      type: "function",
    },
    chain: chainName,
    calls: tokens.map((token) => ({ target: token })),
  });

  const { output: symbols } = await multiCall({
    abi: "erc20:symbol",
    chain: chainName,
    calls: tokens.map((token) => ({ target: token })),
  });

  const { output: decimals } = await multiCall({
    abi: "erc20:decimals",
    chain: chainName,
    calls: tokens.map((token) => ({ target: token })),
  });

  const data = [];

  tokens.forEach((token, i) => {
    const name = names[i];
    const symbol = symbols[i];
    const decimal = decimals[i];

    if (name.success && symbol.success && decimal.success) {
      data.push({
        name: name.output,
        symbol: symbol.output,
        decimals: decimal.output,
        address: token,
        chainId,
        geckoId: null,
        icon: null,
        isGeckoToken: true,
      });
    }
  });

  return [chainId, data];
};
